// 版权归Go作者所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

package buildid

import (
	"bytes"
	"debug/elf"
	"debug/macho"
	"encoding/binary"
	"fmt"
	"io"
	"io/fs"
	"os"
)

func readAligned4(r io.Reader, sz int32) ([]byte, error) {
	full := (sz + 3) &^ 3
	data := make([]byte, full)
	_, err := io.ReadFull(r, data)
	if err != nil {
		return nil, err
	}
	data = data[:sz]
	return data, nil
}

func ReadELFNote(filename, name string, typ int32) ([]byte, error) {
	f, err := elf.Open(filename)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	for _, sect := range f.Sections {
		if sect.Type != elf.SHT_NOTE {
			continue
		}
		r := sect.Open()
		for {
			var namesize, descsize, noteType int32
			err = binary.Read(r, f.ByteOrder, &namesize)
			if err != nil {
				if err == io.EOF {
					break
				}
				return nil, fmt.Errorf("read namesize failed: %v", err)
			}
			err = binary.Read(r, f.ByteOrder, &descsize)
			if err != nil {
				return nil, fmt.Errorf("read descsize failed: %v", err)
			}
			err = binary.Read(r, f.ByteOrder, &noteType)
			if err != nil {
				return nil, fmt.Errorf("read type failed: %v", err)
			}
			noteName, err := readAligned4(r, namesize)
			if err != nil {
				return nil, fmt.Errorf("read name failed: %v", err)
			}
			desc, err := readAligned4(r, descsize)
			if err != nil {
				return nil, fmt.Errorf("read desc failed: %v", err)
			}
			if name == string(noteName) && typ == noteType {
				return desc, nil
			}
		}
	}
	return nil, nil
}

var elfGoNote = []byte("Go\x00\x00")
var elfGNUNote = []byte("GNU\x00")

// Go build ID存储在ELF PT_note prog 
// 头描述的注释中。调用者已经打开文件名以获取f，并在数据中读取
// 至少4KB。
func readELF(name string, f *os.File, data []byte) (buildid string, err error) {
	// 假设注释内容在数据中，已经读取。
	// 重写ELF头，将shnum设置为0，这样我们就可以将数据传递给ELF。新建文件，它将解码Prog列表，但不会解码
	// 尝试从磁盘读取节头和字符串表。
	// 当我们只关心程序列表和一个ELF注释时，这是对I/O的浪费。
	switch elf.Class(data[elf.EI_CLASS]) {
	case elf.ELFCLASS32:
		data[48] = 0
		data[49] = 0
	case elf.ELFCLASS64:
		data[60] = 0
		data[61] = 0
	}

	const elfGoBuildIDTag = 4
	const gnuBuildIDTag = 3

	ef, err := elf.NewFile(bytes.NewReader(data))
	if err != nil {
		return "", &fs.PathError{Path: name, Op: "parse", Err: err}
	}
	var gnu string
	for _, p := range ef.Progs {
		if p.Type != elf.PT_NOTE || p.Filesz < 16 {
			continue
		}

		var note []byte
		if p.Off+p.Filesz < uint64(len(data)) {
			note = data[p.Off : p.Off+p.Filesz]
		} else {
			// 对于某些链接器，例如Solaris链接器，
			// 由于注释段位置的差异，可能在数据（可能包含文件的前16kB）
			// 甚至文件
			// 的前几兆字节中找不到buildid；
			// 在这种情况下，手动提取票据数据。
			_, err = f.Seek(int64(p.Off), io.SeekStart)
			if err != nil {
				return "", err
			}

			note = make([]byte, p.Filesz)
			_, err = io.ReadFull(f, note)
			if err != nil {
				return "", err
			}
		}

		filesz := p.Filesz
		off := p.Off
		for filesz >= 16 {
			nameSize := ef.ByteOrder.Uint32(note)
			valSize := ef.ByteOrder.Uint32(note[4:])
			tag := ef.ByteOrder.Uint32(note[8:])
			nname := note[12:16]
			if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == elfGoBuildIDTag && bytes.Equal(nname, elfGoNote) {
				return string(note[16 : 16+valSize]), nil
			}

			if nameSize == 4 && 16+valSize <= uint32(len(note)) && tag == gnuBuildIDTag && bytes.Equal(nname, elfGNUNote) {
				gnu = string(note[16 : 16+valSize])
			}

			nameSize = (nameSize + 3) &^ 3
			valSize = (valSize + 3) &^ 3
			notesz := uint64(12 + nameSize + valSize)
			if filesz <= notesz {
				break
			}
			off += notesz
			align := p.Align
			alignedOff := (off + align - 1) &^ (align - 1)
			notesz += alignedOff - off
			off = alignedOff
			filesz -= notesz
			note = note[notesz:]
		}
	}

	// 如果我们没有找到Go note，请使用GNU note（如果有）。
	// 这是gccgo使用的。
	if gnu != "" {
		return gnu, nil
	}

	// 没有注释。视为成功，但生成ID为空。
	return "", nil
}

// Go build ID存储在Mach-O__文本段的开头。
// 调用者已经打开了filename，以获取f，并读取了几kB的数据。遗憾的是，这并不能保证能保存这张纸币，因为在主文本之前的文件中有任意数量的其他垃圾。
func readMacho(name string, f *os.File, data []byte) (buildid string, err error) {
	// 如果我们想要的数据已经被读取，不要担心Mach-O解析。
	// 这既是一种优化，也是对Mach-O解析失败的一种对冲。例如，由于_文本部分的名称发生变化，
	// 将来会失败。
	if b, err := readRaw(name, data); b != "" && err == nil {
		return b, err
	}

	mf, err := macho.NewFile(f)
	if err != nil {
		return "", &fs.PathError{Path: name, Op: "parse", Err: err}
	}

	sect := mf.Section("__text")
	if sect == nil {
		// 每个二进制文件都有一个_文本部分。有点不对劲。
		return "", &fs.PathError{Path: name, Op: "parse", Err: fmt.Errorf("cannot find __text section")}
	}

	// 它应该在前几个字节中，但为了以防万一，读了很多，尤其是考虑到我们过去在OS X上的问题，构建ID正在移动。
	// 读取4kB和32kB之间应该没有太大区别：
	// 最困难的部分是获取数据，而不是传输数据。
	n := sect.Size
	if n > uint64(readSize) {
		n = uint64(readSize)
	}
	buf := make([]byte, n)
	if _, err := f.ReadAt(buf, int64(sect.Offset)); err != nil {
		return "", err
	}

	return readRaw(name, buf)
}
